package ru.ifmo.itmokis.sim;

import ru.ifmo.itmokis.util.DataContext;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import org.apache.log4j.Logger;
import ru.ifmo.itmokis.algo.SupplyBuilder;
import ru.ifmo.itmokis.domain.Material;
import ru.ifmo.itmokis.domain.Order;
import ru.ifmo.itmokis.domain.Order.Status;
import ru.ifmo.itmokis.domain.OrderedProduct;
import ru.ifmo.itmokis.domain.Product;
import ru.ifmo.itmokis.domain.Time;
import ru.ifmo.itmokis.exceptions.NotEnoughMaterialsException;
import ru.ifmo.itmokis.util.Config;

/**
 *
 * @author Ivan Stepuk
 */
public class ProductionQueue implements ClockListener {

    private Config config;
    private DataContext dataContext;
    private SupplyBuilder supplyBuilder;
    // ^ Dependencies ^
    private OrderedProduct lastProduced;
    private OrderedProduct current;
    private List<OrderedProduct> plan = new ArrayList<OrderedProduct>();
    private Time progress = Time.ZERO;

    public ProductionQueue(Config config,
            DataContext dataContext,
            SupplyBuilder supplyBuilder) {
        this.config = config;
        //this.facade = facade;
        this.dataContext = dataContext;
        this.supplyBuilder = supplyBuilder;
    }

    @Override
    public void process(Time time) {
        if (current == null) {
            if (!plan.isEmpty()) {
                try {
                    fetchProductFromPlan(time, 0);
                } catch (NotEnoughMaterialsException ex) {
                    Logger.getLogger(getClass()).debug("Not enough materials for next product");
                }
            }
        } else {
            advance(time);
        }
    }

    public void appendProduct(OrderedProduct product) {
        plan.add(product);
    }

    public void insertProduct(OrderedProduct product) {
        if (plan.size() < 2) {
            appendProduct(product);
        } else {
            Random rand = new Random();
            plan.add(rand.nextInt(plan.size()), product);
        }
    }

    public void fetchProductFromPlan(Time currentTime, int index) throws NotEnoughMaterialsException {
        OrderedProduct candidate;
        try {
            candidate = plan.get(index);
        } catch (IndexOutOfBoundsException ex) {
            throw new NotEnoughMaterialsException(plan.get(0));
        }
        if (enoughMaterialsFor(candidate.getProduct().getRecipe())) {
            spendMaterialsFor(candidate);

            // New 'current' product
            current = candidate;
            plan.remove(index);

            // Calculate required time
            current.setStartTime(currentTime);
            progress = current.getProduct().getProductionDuration();

            // Add changeover time if needed
            if (lastProduced != null && !current.getProduct().equals(lastProduced.getProduct())) {
                progress = progress.add(config.getTime("ChangeoverTime"));
            }

            // Update order status
            dataContext.updateOrderStatus(current.getOrder(), Status.IN_PROGRESS);
            advance(currentTime);
        } else {
            supplyBuilder.updatePlan(candidate.getProduct().getRecipe());
            fetchProductFromPlan(currentTime, index + 1);
        }
    }

    public void advance(Time currentTime) {
        // Have current product
        progress = progress.subtract(Time.ONE_HOUR);

        if (progress.compareTo(Time.ZERO) <= 0) {

            // Produced successfully
            current.setFinishTime(currentTime.add(Time.ONE_HOUR));
            dataContext.persist(current);

            // Update order
            Order order = current.getOrder();
            dataContext.advanceOrder(order);

            // Check if order is complete
            if (order.getCompletedProducts() == order.getTotalProducts()) {
                dataContext.setOrderCompleted(order, currentTime.add(Time.ONE_HOUR));
            }

            // Current -> null
            lastProduced = current;
            current = null;
        }
    }

    public void setDeadlines() {
        ListIterator<OrderedProduct> it = plan.listIterator(plan.size());
        while (it.hasPrevious()) {
            OrderedProduct op = it.previous();
            Order o = op.getOrder();
            if (o.getDeadline() == null) {
                switch (o.getType()) {
                    case STANDARD:
                        dataContext.updateOrderDeadline(o, op.getFinishTime().add(config.getTime("ExtraTime")));
                        break;
                    case URGENT:
                        dataContext.updateOrderDeadline(o, op.getFinishTime().add(config.getTime("UrgentExtraTime")));
                        break;
                }
            }
        }
    }

    public boolean enoughMaterialsFor(Map<Material, Integer> recipe) {
        Iterator<Entry<Material, Integer>> it = recipe.entrySet().iterator();
        while (it.hasNext()) {
            Entry<Material, Integer> entry = it.next();
            if (entry.getValue() > entry.getKey().getQty()) {
                return false;
            }
        }
        return true;
    }

    public void spendMaterialsFor(OrderedProduct product) {
        Map<Material, Integer> recipe = product.getProduct().getRecipe();
        Iterator<Entry<Material, Integer>> it = recipe.entrySet().iterator();
        while (it.hasNext()) {
            Entry<Material, Integer> entry = it.next();
            dataContext.updateMaterialQty(entry.getKey(), -entry.getValue());
        }
    }

    public OrderedProduct getCurrent() {
        return current;
    }

    public OrderedProduct getLastProduced() {
        return lastProduced;
    }

    public List<OrderedProduct> getPlan() {
        return plan;
    }

    public void setPlan(List<OrderedProduct> plan) {
        this.plan = plan;
    }
}
